<?php
/**
* RModel class file.
*
* @author Federico Ramirez <fedekiller@gmail.com>
* @link http://code.google.com/p/akaikiwi/
* @copyright Copyright &copy; 2008-2011 Federico Ramirez
* @license http://www.opensource.org/licenses/mit-license.php MIT Licence
*/

/*
* This is a modified RedBean model with built-in validation
* Options for validators:
* on			update, open, delete
* allow_blank	allow blank to satisfy the condition
* message		custom error message
*/
class RModel extends RedBean_SimpleModel {
	protected $validates_prescence_of = array();
	protected $validates_length_of = array();
	protected $validates_inclusion_of = array();
	protected $validates_exclusion_of = array();
	protected $validates_format_of = array();
	protected $validates_numericality_of = array();
	protected $validates_uniqueness_of = array();
	protected $validates_email = array();

	protected $error_preHtml = '<ul>';
	protected $error_postHtml = '</ul>';
	protected $error_html = '<li>{error}</li>';

	/**
	 * Validates the prescence of the required field
	 * @param Array $rule
	 */
	private function validate_prescence_of($rule) {
		$field = $rule[0];

		if(array_key_exists('allow_blank', $rule)) {
			if($this->{$field} == '') {
				return NULL;
			}
		}

		$error_message = array_key_exists('message', $rule) ? $rule['message'] : 'The field ' . $field . ' cannot be blank.';
		return ($this->{$field} == '') ? ($error_message) : NULL;
	}

	/**
	 * Validates the length of the required field
	 * Individual rules:
	 * 		* min
	 * 		* max
	 * 		* in
	 * @param Array $rule
	 */
	private function validate_length_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		$error_message = '';
		$return = NULL;

		if(array_key_exists('min', $rule)) {
			$return = strlen($this->{$field}) >= $rule['min'];
			$error_message = 'Field length is too small! Minimum value is ' . $rule['min'].', ' . strlen($this->{$field}). ' given';
		} else if(array_key_exists('max', $rule)) {
			$return = strlen($this->{$field}) <= $rule['max'];
			$error_message = 'Field length is too big! Maximum value is ' . $rule['max'].', ' . strlen($this->{$field}). ' given';
		} else if(array_key_exists('in', $rule)) {
			$return = (strlen($this->{$field}) >= $rule['in'][0]) && (strlen($this->{$field}) <= $rule['in'][1]);
			$error_message = 'Field length is not within range! It must be between ' . $rule['in'][0] . ' and ' . $rule['in'][1].', ' . strlen($this->{$field}). ' given';
		} else {
			throw new AkaikiwiException('Invalid rule format for validate_length_of.');
		}

		$error_message = array_key_exists('message', $rule) ? $rule['message'] : $error_message;
		return $return ? NULL : ($error_message);
	}

	/**
	 * Validates the prescence of the required field inside the given set
	 * Individual rules:
	 * 		* in
	 * @param Array $rule
	 */
	private function validate_inclusion_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		if(!array_key_exists('in', $rule) || !is_array($rule['in'])) {
			throw new AkaikiwiException('Invalid rule format for validate_inclusion_of.');
		}

		$is = FALSE;
		$items = '';
		foreach($rule['in'] as $item) {
			if($item == $this->{$field}) {
				$is = TRUE;
			}
			$items .= $item . ', ';
		}

		$error_message = array_key_exists('message', $rule) ? $rule['message'] : $field . ' is not in ['.(substr($items, 0, strlen($items) - 2)).']';
		return !$is ? $error_message : NULL;
	}

	/**
	 * Validates if the field is not in the given set
	 * Individual rules:
	 * 		* in
	 * @param Array $rule
	 */
	private function validate_exclusion_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		if(!array_key_exists('in', $rule) || !is_array($rule['in'])) {
			throw new AkaikiwiException('Invalid rule format for validate_inclusion_of.');
		}

		$is = FALSE;
		$items = '';
		foreach($rule['in'] as $item) {
			if($item == $this->{$field}) {
				$is = TRUE;
			}
			$items .= $item . ', ';
		}

		$error_message = array_key_exists('message', $rule) ? $rule['message'] : $field . ' is in ['.(substr($items, 0, strlen($items) - 2)).']';
		return $is ? $error_message : NULL;
	}

	/**
	 * Validates the format of the field
	 * Individual rules:
	 * 		* with
	 * @param Array $rule
	 */
	private function validate_format_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		if(!array_key_exists('with', $rule)) {
			throw new AkaikiwiException('Invalid rule format for validate_prescence_of');
		}

		$error_message = NULL;
		if(!preg_match('#' . $rule['with'] . '#', $this->{$field})) {
			$error_message .= 'Field '.$field.' does not follow the specified format: ' . $rule['with'];
		}

		$error_message = (array_key_exists('message', $rule) && $error_message != NULL) ? $rule['message'] : $error_message;
		return $error_message;
	}

	/**
	 * Validates the numericality of the field
	 * Individual rules:
	 * 		* only_integer
	 * 		* even
	 * 		* odd
	 * 		* greater_than
	 * 		* greater_or_equal_than
	 * 		* smaller_than
	 * 		* smaller_or_equal_than
	 * 		* equal_to
	 * @param Array $rule
	 */
	private function validate_numericality_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		$error = NULL;
		$field_val = $this->{$field};

		foreach($rule as $key => $value) {
			if($key === 'only_integer') {
				if(!(is_numeric($field_val) !== FALSE) || !is_int((int)$field_val)) {
					$error .= $field . '('.$field_val.') is not an integer';
					break;
				}
			} else if($key === 'even') {
				if(!($field_val % 2 == 0)) {
					$error .= $field . ' is not even';
					break;
				}
			} else if($key === 'odd') {
				if(!($field_val % 2 == 1)) {
					$error .= $field . ' is not odd';
					break;
				}
			} else if($key === 'greater_than') {
				echo $field_val .' > ' . $value. '<br />';
				if(!($field_val > $value)) {
					$error .= $field . ' is not greater than ' . $value;
					break;
				}
			} else if($key === 'greater_or_equal_than') {
				if(!($field_val >= $value)) {
					$error .= $field . ' is not greater or equal than ' . $value;
					break;
				}
			} else if($key === 'smaller_than') {
				if(!($field_val < $value)) {
					$error .= $field . ' is not smaller than ' . $value;
					break;
				}
			} else if($key === 'smaller_or_equal_than') {
				if(!($field_val <= $value)) {
					$error .= $field . ' is not smaller or equal than ' . $value;
					break;
				}
			} else if($key === 'equal_to') {
				if(!($field_val == $value)) {
					$error .= $field . ' is not equal to ' . $value;
					break;
				}
			}
		}

		return $error;
	}

	/**
	 * Validates the uniqueness of the fuild
	 * Individual rules
	 * 		* table_name
	 * @param Array $rule
	 */
	private function validate_uniqueness_of($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		if(!array_key_exists('table_name', $rule)) {
			throw new AkaikiwiException('Invalid rule format for validate_uniqueness_of');
		}

		$db = R::$adapter;
		$posts = $db->exec('select * from '.$rule['table_name']. " where $field='".$this->{$field}."'");
		$error_message = NULL;
		if($posts > 0) {
			$error_message = array_key_exists('message', $rule) ? $rule['message'] : 'Field ' . $field . ' already exists in ' . $rule['table_name'];
		}

		return $error_message;
	}

	/**
	* Validates an email adress
	* Individial rules:
	* 	* None
	* @param Array $rule
	*/
	private function validate_email($rule) {
		$field = $rule[0];

		if($this->{$field} == '') {
			if(array_key_exists('allow_blank', $rule)) {
				return NULL;
			} else {
				return 'The field ' . $field . ' cannot be blank.';
			}
		}

		$error_message = 'The field '. $field . ' is an invalid e-mail';

		$email = $this->{$field};
		if (!preg_match("#^[^@]{1,64}@[^@]{1,255}$#", $email)) {
			return $error_message;
		}
		
		$email_array = explode("@", $email);
		$local_array = explode(".", $email_array[0]);
		for ($i = 0; $i < sizeof($local_array); $i++) {
			if(!preg_match("@^(([A-Za-z0-9!#$%&'*+/=?^_`{|}~-][A-Za-z0-9!#$%&↪'*+/=?^_`{|}~\.-]{0,63})|(\"[^(\\|\")]{0,62}\"))$@", $local_array[$i])) {
			  return $error_message;
			}
		}
		
		if (!preg_match("#^\[?[0-9\.]+\]?$#", $email_array[1])) {
			$domain_array = explode(".", $email_array[1]);
			if (sizeof($domain_array) < 2) {
				return $error_message; // Not enough parts to domain
			}
			for ($i = 0; $i < sizeof($domain_array); $i++) {
				  if(!preg_match("#^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|↪([A-Za-z0-9]+))$#", $domain_array[$i])) {
					return $error_message;
				  }
			}
		}
		
		return NULL;
	}

	/**
	 * Verify that the given rule must be applied in this event.
	 * @param String $name
	 * @param String $rule
	 */
	private function verify_event($name, $rule) {
		if(array_key_exists('on', $rule)) {
			$events = explode(',', $rule['on']);
			$found = FALSE;
			foreach($events as $event) {
				$event = trim($event);
				if($event == $name || $event == $_GET['action']) {
					$found = TRUE;
				}
			}
			return $found;
		}
		return TRUE;
	}

	/**
	 * Run all tests on the given event
	 * @param String $event
	 */
	private function run_tests($event) {
		$errors = array();

		$tests = array(
			'validates_prescence_of'	=>	'validate_prescence_of', 
			'validates_length_of'		=>	'validate_length_of',
			'validates_inclusion_of'	=>	'validate_inclusion_of',
			'validates_exclusion_of'	=>	'validate_exclusion_of',
			'validates_format_of'		=>	'validate_format_of',
			'validates_numericality_of'	=>	'validate_numericality_of',
			'validates_uniqueness_of'	=>	'validate_uniqueness_of',
			'validates_email'			=>	'validate_email',
		);

		foreach($tests as $k => $v) {
			foreach($this->{$k} as $rule) {
				if(!$this->verify_event($event, $rule)) {
					continue;
				}
				$result = $this->{$v}($rule);
				if($result != NULL) {
					$errors[] = $result;
				}
			}
		}

		return $errors;
	}

	private function format_errors($errors) {
		$r = $this->error_preHtml;
		foreach($errors as $error) {
			$r .= str_replace('{error}', $error, $this->error_html);
		}
		$r .= $this->error_postHtml;
		return $r;
	}

	public function update() {
		$errors = $this->run_tests('update');

		if(count($errors) > 0) {
			throw new AkaikiwiException($this->format_errors($errors));
		}
	}

	public function open() {
		$errors = $this->run_tests('open');

		if(count($errors) > 0) {
			throw new AkaikiwiException($this->format_errors($errors));
		}
	}

	public function delete() {
		$errors = $this->run_tests('delete');

		if(count($errors) > 0) {
			throw new AkaikiwiException($this->format_errors($errors));
		}
	}
}
